package scenery;

import com.crunch.core.Renderer;
import com.crunch.graphics.gpu.GpuBuffer;
import com.crunch.graphics.gpu.GpuState;
import com.crunch.graphics.gpu.GpuTexture;

import javax.media.opengl.GL;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * Used to draw text.
 */
public class Text {
	public Text(int capacity, GpuTexture font, Renderer renderer) {
		this.font = font;
		this.renderer = renderer;

		horizAlign = HorizAlign.LEFT;
		vertAlign = VertAlign.TOP;

		commandPending = false;

		this.initialCapacity = capacity * 6 * VERTEX_SIZE;
		commandInit = true;
		queueCommand();
	}

	public void release() {
		commandRelease = true;
		queueCommand();
	}

	public String getText() {
		return text;
	}

	public HorizAlign getHorizAlign() {
		return horizAlign;
	}

	public VertAlign getVertAlign() {
		return vertAlign;
	}

	public void setText(String text) {
		this.text = text;
		commandSetText = true;
		queueCommand();
	}

	public void setTextAlign(HorizAlign horizAlign, VertAlign vertAlign) {
		if (this.horizAlign != horizAlign || this.vertAlign != vertAlign) {
			this.horizAlign = horizAlign;
			this.vertAlign = vertAlign;
			queueCommand();
		}
	}

	public GpuBuffer getVertexBuffer() {
		return vertexBuffer;
	}

	public int getVertexCount() {
		return vertexCount;
	}

	public GpuTexture getFont() {
		return font;
	}

	public enum HorizAlign {
		LEFT,
		CENTER,
		RIGHT
	}

	public enum VertAlign {
		TOP,
		MIDDLE,
		BOTTOM
	}

	private Renderer renderer;
	private GpuBuffer vertexBuffer;
	private int vertexCount;
	private GpuTexture font;
	private String text;
	private HorizAlign horizAlign;
	private VertAlign vertAlign;

	private boolean commandPending;
	// queues command if it has not yet been queued
	private void queueCommand() {
		if (commandPending) {
			return;
		}
		renderer.queueCommand(new Renderer.Command() {
			@Override
			public void execute(GpuState gpuState) {
				command(gpuState);
			}
		});
		commandPending = true;
	}

	private int initialCapacity;

	private boolean commandInit = false;
	private boolean commandSetText = false;
	private boolean commandRelease = false;

	private void command(GpuState gpuState) {
		commandPending = false;

		// allocate empty buffer if we are initializing and no text has been set
		if (commandInit) {
			if (font != null) {
				font.acquire();
			}
			if (text == null) {
				GpuBuffer.BufferDescription desc = new GpuBuffer.BufferDescription();
				desc.type = GL.GL_ARRAY_BUFFER;
				desc.size = initialCapacity * 6 * VERTEX_SIZE;
				desc.usage = GL.GL_STATIC_DRAW;
				vertexBuffer = gpuState.createBuffer(desc);
			}
		}

		// update the buffer if text was set
		if (commandSetText && text != null) {
			byte[] vertexData = getTextVertexData();

			// check if we need to reallocate
			if (vertexBuffer != null && vertexBuffer.getSize() < vertexData.length) {
				vertexBuffer.release();
				vertexBuffer = null;
			}

			if (vertexBuffer == null) {
				// allocate buffer with enough data for characters
				GpuBuffer.BufferDescription desc = new GpuBuffer.BufferDescription();
				desc.type = GL.GL_ARRAY_BUFFER;
				desc.size = vertexData.length;
				desc.data = ByteBuffer.wrap(vertexData);
				desc.usage = GL.GL_STATIC_DRAW;
				vertexBuffer = gpuState.createBuffer(desc);
			} else {
				// write to existing buffer
				vertexBuffer.write(0, vertexData.length, ByteBuffer.wrap(vertexData));
			}

			vertexCount = vertexData.length / VERTEX_SIZE;
		}

		if (text == null) {
			vertexCount = 0;
		}

		if (commandRelease) {
			if (font != null) {
				font.release();
			}
			if (vertexBuffer != null) {
				vertexBuffer.release();
			}
		}
	}

	private byte[] getTextVertexData() {
		if (font == null) {
			return new byte[0];
		}

		// first determine number of characters required
		// don't count newlines
		int charCount = 0;
		for (int i = 0; i < text.length(); ++i) {
			if (text.charAt(i) != '\n') {
				++charCount;
			}
		}
		byte[] vertexData = new byte[charCount * 6 * VERTEX_SIZE];
		ByteBuffer buf = ByteBuffer.wrap(vertexData);
		buf.order(ByteOrder.nativeOrder());

		final int FONT_ROWS = 8;
		final int FONT_COLS = 16;

		int charWidth = font.getWidth() / FONT_COLS;
		int charHeight = font.getHeight() / FONT_ROWS;

		// count string height
		int stringHeight = charHeight;
		for (int i = 0; i < text.length(); ++i) {
			if (text.charAt(i) == '\n') {
				stringHeight += charHeight;
			}
		}

		// add each character
		float charTexWidth = 1.0f / (float) FONT_COLS;
		float charTexHeight = 1.0f / (float) FONT_ROWS;

		int baseX = 0;
		int baseY = 0;
		switch (horizAlign) {
			case LEFT:
				baseX = 0;
				break;
			case RIGHT:
				baseX = -getLineLength(text, 0) * charWidth;
				break;
			case CENTER:
				baseX = -(getLineLength(text, 0) * charWidth)/2;
				break;
		}
		switch (vertAlign) {
			case TOP:
				baseY = -charHeight;
				break;
			case MIDDLE:
				baseY = stringHeight/2 - charHeight;
				break;
			case BOTTOM:
				baseY = stringHeight - charHeight;
				break;
		}

		for (int i = 0; i < text.length(); ++i) {
			char c = text.charAt(i);
			if (c == '\n') {
				// increment line
				++i;
				switch (horizAlign) {
					case LEFT:
						baseX = 0;
						break;
					case RIGHT:
						baseX = -getLineLength(text, i) * charWidth;
						break;
					case CENTER:
						baseX = -(getLineLength(text, i) * charWidth)/2;
						break;
				}
				baseY -= charHeight;
			} else {
				// add character data
				if (c >= FONT_ROWS * FONT_COLS) {
					c = 0;
				}
				int charRow = c / FONT_COLS;
				int charCol = c % FONT_COLS;

				// (0,0) is at bottom, but we are indexing from top left, so flip row
				charRow = FONT_ROWS - charRow - 1;
				float uA = (float) charCol / (float) FONT_COLS;
				float vA = (float) charRow / (float) FONT_ROWS;
				float uB = uA + charTexWidth;
				float vB = vA + charTexHeight;

				float xA = (float) baseX;
				float yA = (float) baseY;
				float xB = xA + (float) charWidth;
				float yB = yA + (float) charHeight;

				// 2 triangles (6 vertices)

				buf.putFloat(xA);
				buf.putFloat(yA);
				buf.putFloat(uA);
				buf.putFloat(vA);

				buf.putFloat(xB);
				buf.putFloat(yA);
				buf.putFloat(uB);
				buf.putFloat(vA);

				buf.putFloat(xA);
				buf.putFloat(yB);
				buf.putFloat(uA);
				buf.putFloat(vB);

				buf.putFloat(xA);
				buf.putFloat(yB);
				buf.putFloat(uA);
				buf.putFloat(vB);

				buf.putFloat(xB);
				buf.putFloat(yA);
				buf.putFloat(uB);
				buf.putFloat(vA);

				buf.putFloat(xB);
				buf.putFloat(yB);
				buf.putFloat(uB);
				buf.putFloat(vB);

				baseX += charWidth;
			}
		}

		return vertexData;
	}

	private int getLineLength(String str, int start) {
		for (int i = start; i < str.length(); ++i) {
			if (str.charAt(i) == '\n') {
				return i - start;
			}
		}
		return str.length() - start;
	}

	// vertex information
	public static final int POSITION_OFFSET = 0;
	public static final int UV_OFFSET       = 8;
	public static final int VERTEX_SIZE     = 16;
}
